using System;
using System.Collections;
using System.Collections.Generic;
using static LightCAD.MathLib.Constants;
using static LightCAD.MathLib.MathEx;

namespace LightCAD.Three
{
    public class KeyframeTrack
    {
        #region scope properties or methods

        //public static JObject toJSON(KeyframeTrack track)
        //{

        //    JObject json;

        //    // derived classes can define a static toJSON method
        //    if (track.HasMethod("toJSON"))
        //    {

        //        json = (JObject)track.InvokeMethod("toJSON");

        //    }
        //    else
        //    {

        //        // by default, we assume the data can be serialized as-is
        //        json = new JObject();

        //        json.Add("name", track.name);
        //        json.Add("times", AnimationUtils.toJArray(track.times));

        //        json.Add("values", AnimationUtils.toJArray(track.values));

        //        var interpolation = track.getInterpolation();
        //        if (interpolation != track.DefaultInterpolation)
        //        {
        //            json.Add("interpolation", interpolation);
        //        }

        //    }

        //    json.Add("type", track.ValueTypeName); // mandatory

        //    return json;

        //}

        #endregion

        #region Properties

        public string name;

        public double[] times;
        public double[] values;
        public string ValueTypeName;
        public string ValueBufferType;
        public string TimeBufferType;
        public int DefaultInterpolation = InterpolateLinear;
        public Func<double[], Interpolant> createInterpolant;
        #endregion

        #region constructor
        public KeyframeTrack(string name, double[] times, double[] values, int? interpolation)
        {
            if (name == null) throw new Error("THREE.KeyframeTrack: track name is undefined");
            if (times == null || times.Length == 0) throw new Error("THREE.KeyframeTrack: no keyframes in track named " + name);
            this.name = name;
            this.times = times;// AnimationUtils.convertArray(times, this.TimeBufferType);
            this.values = values;// AnimationUtils.convertArray(values, this.ValueBufferType);
            this.setInterpolation(interpolation ?? this.DefaultInterpolation);
        }
        #endregion

        #region methods

        public DiscreteInterpolant InterpolantFactoryMethodDiscrete(double[] result)
        {
            return new DiscreteInterpolant(this.times, this.values, this.getValueSize(), result);
        }
        public virtual Interpolant InterpolantFactoryMethodLinear(double[] result)
        {
            return new LinearInterpolant(this.times, this.values, this.getValueSize(), result);
        }
        public virtual Interpolant InterpolantFactoryMethodSmooth(double[] result)
        {
            return new CubicInterpolant(this.times, this.values, this.getValueSize(), result);
        }
        public KeyframeTrack setInterpolation(int interpolation)
        {
            Func<double[], Interpolant> factoryMethod = null;
            switch (interpolation)
            {
                case InterpolateDiscrete:
                    factoryMethod = this.InterpolantFactoryMethodDiscrete;
                    break;
                case InterpolateLinear:
                    factoryMethod = this.InterpolantFactoryMethodLinear;
                    break;
                case InterpolateSmooth:
                    factoryMethod = this.InterpolantFactoryMethodSmooth;
                    break;
            }
            if (factoryMethod == null)
            {
                var message = "unsupported interpolation for " +
                    this.ValueTypeName + " keyframe track named " + this.name;
                if (this.createInterpolant == null)
                {
                    // fall back to default, unless the default itself is messed up
                    if (interpolation != this.DefaultInterpolation)
                    {
                        this.setInterpolation(this.DefaultInterpolation);
                    }
                    else
                    {
                        throw new Error(message); // fatal, in this case
                    }
                }
                console.warn("THREE.KeyframeTrack:", message);
                return this;
            }
            this.createInterpolant = factoryMethod;
            return this;
        }
        public int getInterpolation()
        {
            if (this.createInterpolant == this.InterpolantFactoryMethodDiscrete)
                return InterpolateDiscrete;
            if (this.createInterpolant == this.InterpolantFactoryMethodLinear)
                return InterpolateLinear;
            if (this.createInterpolant == this.InterpolantFactoryMethodSmooth)
                return InterpolateSmooth;
            return 0;
        }
        public int getValueSize()
        {
            return this.values.Length / this.times.Length;
        }
        public KeyframeTrack shift(double timeOffset)
        {
            if (timeOffset != 0.0)
            {
                var times = this.times;
                for (int i = 0, n = times.Length; i != n; ++i)
                {
                    times[i] += timeOffset;
                }
            }
            return this;
        }
        public KeyframeTrack scale(double timeScale)
        {
            if (timeScale != 1.0)
            {
                var times = this.times;
                for (int i = 0, n = times.Length; i != n; ++i)
                {
                    times[i] *= timeScale;
                }
            }
            return this;
        }
        public KeyframeTrack trim(double startTime, double endTime)
        {
            var times = this.times;
            var nKeys = times.Length;
            var from = 0;
            var to = nKeys - 1;
            while (from != nKeys && times[from] < startTime)
            {
                ++from;
            }
            while (to != -1 && times[to] > endTime)
            {
                --to;
            }
            ++to; // inclusive -> exclusive bound
            if (from != 0 || to != nKeys)
            {
                // empty tracks are forbidden, so keep at least one keyframe
                if (from >= to)
                {
                    to = Math.Max(to, 1);
                    from = to - 1;
                }
                var stride = this.getValueSize();
                this.times = AnimationUtils.arraySlice(times, from, to);
                this.values = AnimationUtils.arraySlice(this.values, from * stride, to * stride);
            }
            return this;
        }
        public bool validate()
        {
            var valid = true;
            var valueSize = this.getValueSize();
            if (valueSize - Math.Floor((double)valueSize) != 0)
            {
                console.error("THREE.KeyframeTrack: Invalid value size in track.", this);
                valid = false;
            }
            var times = this.times;
            var values = this.values;
            var nKeys = times.Length;
            if (nKeys == 0)
            {
                console.error("THREE.KeyframeTrack: Track is empty.", this);
                valid = false;
            }
            double prevTime = double.NaN;
            for (int i = 0; i != nKeys; i++)
            {
                var currTime = times[i];
                if (IsNaN(currTime))
                {
                    console.error("THREE.KeyframeTrack: Time is not a valid number.", this, i, currTime);
                    valid = false;
                    break;
                }
                if (prevTime != double.NaN && prevTime > currTime)
                {
                    console.error("THREE.KeyframeTrack: Out of order keys.", this, i, currTime, prevTime);
                    valid = false;
                    break;
                }
                prevTime = currTime;
            }
            if (values != null)
            {
                for (int i = 0, n = values.Length; i != n; ++i)
                {
                    var value = values[i];
                    if (IsNaN(value))
                    {
                        console.error("THREE.KeyframeTrack: Value is not a valid number.", this, i, value);
                        valid = false;
                        break;
                    }
                }
            }
            return valid;
        }
        public KeyframeTrack optimize()
        {
            // times or values may be shared with other tracks, so overwriting is unsafe
            var times = AnimationUtils.arraySlice(this.times);
            var values = AnimationUtils.arraySlice(this.values);
            var stride = this.getValueSize();
            var smoothInterpolation = this.getInterpolation() == InterpolateSmooth;
            var lastIndex = times.Length - 1;
            var writeIndex = 1;
            for (int i = 1; i < lastIndex; ++i)
            {
                var keep = false;
                var time = times[i];
                var timeNext = times[i + 1];
                // remove adjacent keyframes scheduled at the same time
                if (time != timeNext && (i != 1 || time != times[0]))
                {
                    if (!smoothInterpolation)
                    {
                        // remove unnecessary keyframes same as their neighbors
                        var offset = i * stride;
                        var offsetP = offset - stride;
                        var offsetN = offset + stride;
                        for (int j = 0; j != stride; ++j)
                        {
                            var value = values[offset + j];
                            if (value != values[offsetP + j] ||
                                value != values[offsetN + j])
                            {
                                keep = true;
                                break;
                            }
                        }
                    }
                    else
                    {
                        keep = true;
                    }
                }
                // in-place compaction
                if (keep)
                {
                    if (i != writeIndex)
                    {
                        times[writeIndex] = times[i];
                        var readOffset = i * stride;
                        var writeOffset = writeIndex * stride;
                        for (int j = 0; j != stride; ++j)
                        {
                            values[writeOffset + j] = values[readOffset + j];
                        }
                    }
                    ++writeIndex;
                }
            }
            // flush last keyframe (compaction looks ahead)
            if (lastIndex > 0)
            {
                times[writeIndex] = times[lastIndex];
                for (int readOffset = lastIndex * stride, writeOffset = writeIndex * stride, j = 0; j != stride; ++j)
                {
                    values[writeOffset + j] = values[readOffset + j];
                }
                ++writeIndex;
            }
            if (writeIndex != times.Length)
            {
                this.times = AnimationUtils.arraySlice(times, 0, writeIndex);
                this.values = AnimationUtils.arraySlice(values, 0, writeIndex * stride);
            }
            else
            {
                this.times = times;
                this.values = values;
            }
            return this;
        }
        public KeyframeTrack clone()
        {
            var times = AnimationUtils.arraySlice(this.times, 0);
            var values = AnimationUtils.arraySlice(this.values, 0);
            KeyframeTrack track = AnimationUtils.CreateKeyframeTrack(this.GetType().Name, this.name, times, values);
            // Interpolant argument to constructor is not saved, so copy the factory method directly.
            if (track != null)
            {
                track.createInterpolant = this.createInterpolant;
            }
            return track;
        }
        #endregion

    }
}
